﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="DeviceController.cs" company="Microsoft Corporation">
//   Copyright 2013 Microsoft Corporation 
// </copyright>
// <summary>
//   Defines the ValuesController type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace DeviceSync.Controllers
{
    using DeviceSync.Utilities;
    using Newtonsoft.Json;
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Security.Cryptography;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.Http;

    /// <summary>
    /// The device controller.
    /// </summary>
    public class DeviceController : ApiController
    {
        private static string LOCAL_UPLOAD_PATH = @"C:\UploadPath";

        public delegate void MergeCompletedDelegate(string filePath);
        public event MergeCompletedDelegate CompletedMerge;

        /// <summary>
        /// Initializes a new instance of the <see cref="DeviceController"/> class.
        /// </summary>
        public DeviceController()
        {
            //this.RootLocalUploadPath = ConfigurationManager.AppSettings["RootLocalUploadPath"];
            this.RootLocalUploadPath = LOCAL_UPLOAD_PATH;
            this.QueuedLogsPath = this.RootLocalUploadPath + Path.DirectorySeparatorChar + "Queued";
        }

        /// <summary>
        /// Gets the root local upload path.
        /// </summary>
        public string RootLocalUploadPath { get; private set; }

        private string QueuedLogsPath { get; set; }
        private string ErrorLog
        {
            get
            {
                return this.RootLocalUploadPath + Path.DirectorySeparatorChar + "ErrorLogs" + Path.DirectorySeparatorChar + DateTime.Now.ToString("yyyy-MM-dd_hh-mm-ss");
            }
        }

        /// <summary>
        /// The download.
        /// </summary>
        /// <param name="id">
        /// The id.
        /// </param>
        /// <returns>
        /// The <see cref="HttpResponseMessage"/>.
        /// </returns>
        [HttpGet]
        public IEnumerable<string> Get(string id)
        {
            var folderPath = Path.Combine(this.RootLocalUploadPath, id);

            if (!Directory.Exists(folderPath))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            return Directory.EnumerateFiles(folderPath).Select(Path.GetFileName).ToList();
        }


        /// <summary>
        /// The download.
        /// </summary>
        /// <param name="id">
        /// The id.
        /// </param>
        /// <param name="fileName">
        /// The file Name.
        /// </param>
        /// <returns>
        /// The <see cref="HttpResponseMessage"/>.
        /// </returns>
        [HttpGet]
        public HttpResponseMessage Get(string id, string fileName)
        {
            HttpResponseMessage content = null;

            var filePath = Path.Combine(this.RootLocalUploadPath, id, fileName);

            if (!File.Exists(filePath))
            {
                content = new HttpResponseMessage(HttpStatusCode.NotFound);
            }
            else
            {
                var stream = new FileStream(filePath, FileMode.Open);
                content = new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StreamContent(stream)
                };
            }

            return content;
        }

        private void ProcessLogs(string directory, string pathToDoneFile, string uniqueIdentifier)
        {
            string filename = uniqueIdentifier + "_" + DateTime.Now.ToString("yyyy-MM-dd_hh-mm-ss");

            try
            {
                //File.Delete(pathToDoneFile);

                MergeFiles(directory, filename);

                UploadToAnalyzer(this.QueuedLogsPath + Path.DirectorySeparatorChar + filename);
            }
            catch (Exception e)
            {
                Debug.Print("An unexpected exception was thrown. {0}", e.Message);
                File.WriteAllText(this.ErrorLog, e.ToString());
            }

            string[] files = Directory.GetFiles(directory);

            for (int i = 0; i < files.Length; i++)
            {
                File.Delete(files[i]);
            }
        }

        private string Checksum(string content)
        {
            using (var md5 = MD5.Create())
            {
                byte[] hash = md5.ComputeHash(System.Text.Encoding.Default.GetBytes(content));

                return Convert.ToBase64String(hash);
                //return BitConverter.ToString(hash).Replace("-", "");
            }
        }

        private string Checksum(string filePath, bool isFile = true)
        {
            var contents = "";
            using (StreamReader reader = new StreamReader(filePath))
            {
                contents = reader.ReadToEnd();
            }

            return this.Checksum(contents);
        }

        private void ValidateFormat(string content)
        {
            if(content == "[" || content == "]")
                return;

            if(content[content.Length - 1] == ',')
                content = content.Substring(0, content.Length - 1);

            try
            {
                JsonConvert.DeserializeObject("[" + content + "]");
            }
            catch (Newtonsoft.Json.JsonReaderException ex)
            {
                throw new System.FormatException("The sumbitted file type is not supported.", ex);
            }
        }

        public void Put(string id, string checksum, string content)
        {
            var preChecksum = this.Checksum(content);
            if (checksum != preChecksum)
                throw new System.ArgumentException("The expected hash(" + checksum + ") does not match the uploaded hash(" + preChecksum + ")");

            try
            {
                this.ValidateFormat(content);
            }
            catch (System.FormatException ex)
            {
                Debug.Print("Not a valid '.ilf' file. {0}", ex.Message);
                throw;
            }

            var uploadPath = Path.Combine(this.RootLocalUploadPath, id);

            if (!Directory.Exists(uploadPath))
            {
                Directory.CreateDirectory(uploadPath);
            }

            /*
            // The files are named numerically as they are transferred to the gateway.  If there are no files in the specific directory, the file is named "1".
            try
            {
                totalFiles = Directory.GetFiles(uploadPath).Length + 1;
            }
            catch
            {
                totalFiles = 0;
            }
            */

            DateTime timestamp = DateTime.Now;

            var filePath = Path.Combine(uploadPath, timestamp.ToString("yyyy-MM-dd_HH-mm-ss_fffffff"));

            File.WriteAllText(filePath, content);

            var postChecksum = this.Checksum(filePath, true);
            if (checksum != postChecksum)
            {
                File.Delete(filePath);
                throw new System.ArgumentException("The expected hash(" + checksum + ") does not match the uploaded hash(" + postChecksum + ") for " + filePath, "content");
            }
            // If the last written file contains the message "Done", delete the file and merge the rest.
            if (content == "]")
                this.ProcessLogs(uploadPath, filePath, id);

            
        }

        private string GetBody(string boundary, string content)
        {
            var prefix = "\r\n\r\n";
            var suffix = "\r\n--" + boundary + "--";
            int startIndex = content.IndexOf(prefix) + prefix.Length;
            int endIndex = content.IndexOf(suffix) - startIndex;

            return content.Substring(startIndex, endIndex);
        }

        /// <summary>
        /// The upload log file.
        /// </summary>
        /// <param name="id">
        /// The device Id.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        [HttpPost]
        public Task<HttpResponseMessage> Put(string id, string checksum)
        {
            if (id == null || id.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
            {
                throw new HttpResponseException(HttpStatusCode.NotImplemented);
            }

            var hash = HttpUtility.UrlDecode(checksum);

            if (Request.Content.IsMimeMultipartContent())
            {
                string TEMP_DIR = Path.GetTempPath();

                var streamProvider = new MultipartFormDataStreamProvider(TEMP_DIR);

                var task = Request.Content.ReadAsStreamAsync().ContinueWith(t =>
                    {
                        if (t.IsFaulted || t.IsCanceled)
                        {
                            throw new HttpResponseException(HttpStatusCode.InternalServerError);
                        }

                        var stream = t.Result;
                        var results = "";
                        using (StreamReader reader = new StreamReader(stream))
                        {
                            results = reader.ReadToEnd();
                        }

                        var boundary = Request.Content.Headers.ContentType.Parameters.Where(paramter => { return ( paramter.Name == new NameValueHeaderValue("boundary").Name ); }).FirstOrDefault();
                        var content = this.GetBody(boundary.Value.Replace("\"",""), results);

                        try
                        {
                            this.Put(id, hash, content);
                        }
                        catch (System.ArgumentException ex)
                        {
                            Debug.Print(ex.Message);
                            return new HttpResponseMessage(HttpStatusCode.BadRequest);
                        }
                        catch (System.FormatException ex)
                        {
                            Debug.Print(ex.Message);
                            return new HttpResponseMessage(HttpStatusCode.UnsupportedMediaType);
                        }

                        return new HttpResponseMessage(HttpStatusCode.OK);
                    });

                return task;
                /*
                var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(t =>
                {
                    if (t.IsFaulted || t.IsCanceled)
                    {
                        throw new HttpResponseException(HttpStatusCode.InternalServerError);
                    }

                    foreach (MultipartFileData file in streamProvider.FileData)
                    {
                        var contents = File.ReadAllText(file.LocalFileName);

                        try
                        {
                            this.Put(id, hash, contents);
                        }
                        catch (System.ArgumentException ex)
                        {
                            Debug.Print(ex.Message);
                            return new HttpResponseMessage(HttpStatusCode.BadRequest);
                        }
                        catch (System.FormatException ex)
                        {
                            Debug.Print(ex.Message);
                            return new HttpResponseMessage(HttpStatusCode.UnsupportedMediaType);
                        }
                    }

                    if (t.IsFaulted || t.IsCanceled)
                    {
                        throw new HttpResponseException(HttpStatusCode.InternalServerError);
                    }

                    return new HttpResponseMessage(HttpStatusCode.OK);
                });

                return task;
                 * */
            }
            else
            {
                var task = Request.Content.ReadAsStringAsync().ContinueWith(t =>
                {
                    if (t.IsFaulted || t.IsCanceled)
                    {
                        throw new HttpResponseException(HttpStatusCode.InternalServerError);
                    }

                    try
                    {
                        this.Put(id, hash, t.Result);
                    }
                    catch (System.ArgumentException ex)
                    {
                        Debug.Print(ex.Message);
                        return new HttpResponseMessage(HttpStatusCode.BadRequest);
                    }
                    catch (System.FormatException ex)
                    {
                        Debug.Print(ex.Message);
                        return new HttpResponseMessage(HttpStatusCode.UnsupportedMediaType);
                    }

                    return new HttpResponseMessage(HttpStatusCode.OK);
                });

                return task;
            }
        }

        private Dictionary<string, int> fileAttempts = new Dictionary<string, int>();

        /// <summary>
        /// Checks to see if a given file is the Done file, which simply contains the word "Done".
        /// </summary>
        /// <param name="path">Path name of the file in question.</param>
        /// <returns>True if the file is the Done file, false otherwise.</returns>
        private bool CheckDone(string path)
        {
            try
            {
                if (!fileAttempts.ContainsKey(path))
                    fileAttempts.Add(path, 1);
                else
                    fileAttempts[path]++;

                if (fileAttempts[path] >= 4)
                {
                    Debug.Print("Could not open {0}", path);
                    return false;
                }

                string contents = File.ReadAllText(path);

                return String.Equals(contents, "Done", StringComparison.OrdinalIgnoreCase);
            }
            catch (System.IO.IOException)
            {
                return this.CheckDone(path);
            }
        }

        private void MergeFiles(string directory, string filename)
        {
            string[] files = Directory.GetFiles(directory);
            Array.Sort(files, new NaturalStringComparer());

            StringBuilder sb = new StringBuilder();
            using (StreamWriter writer = new StreamWriter(new FileStream(this.QueuedLogsPath + Path.DirectorySeparatorChar + filename, FileMode.Append, FileAccess.Write, (FileShare.Read | FileShare.Delete) )))
            {
                foreach (string path in files)
                {
                    using (StreamReader reader = new StreamReader(path))
                    {
                        sb.Append(reader.ReadToEnd());
                    }
                }

                sb.Replace(",]","]");
                writer.Write(sb.ToString());
            }

            this.CompletedMerge(this.QueuedLogsPath + Path.DirectorySeparatorChar + filename);
        }

        /// <summary>
        /// Uploads newly merged CSV file to the online analyzer to generate a JSON report.
        /// </summary>
        /// <param name="path">Path name of the merged data.</param>
        private void UploadToAnalyzer(string path)
        {
            StorageService.StorageServiceClient storageClient = new StorageService.StorageServiceClient(); // To be used under normal conditions
            //StorageServiceDebug.StorageServiceClient storageClient = new StorageServiceDebug.StorageServiceClient(); // Only use if Azure service is hosted locally

            string csvData = null;

            using (StreamReader reader = new StreamReader(path))
            {
                csvData = reader.ReadToEnd();
            }

            try
            {
                storageClient.UploadLog(csvData);
            }
            catch (System.ArgumentException e)
            {
                using (StreamWriter writer = new StreamWriter(new FileStream(this.ErrorLog, FileMode.Append, FileAccess.Write)))
                {
                    writer.Write(e.ToString());
                }
            }

        }
    }
}